package pt.uma.gap.graph;

import java.io.File;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;



/**
 * Transform graph class representation in XML graph representation
 * 
 * @author Duarte Abreu
 *
 */
public class XMLGraphTransformer {

	private static Logger logger = Logger.getLogger(XMLGraphTransformer.class.getName());

	private Document doc;


	/**Generate DOM graph only with classes, methods and methods calling sequence.
	 * @param classNodeList
	 * @throws Exception
	 */
	public void generateBasicGraph(List<ClassNode> classNodeList) throws Exception {


		doc = getNewDocument();

		//Creating the XML tree

		//create the root element and add it to the document
		Element root = doc.createElement("root");
		doc.appendChild(root);



		for (ClassNode classNode : classNodeList) {

			//Get or create package and classes Node
			Element classesElement = getClassesElement(root, classNode);

			Element classElement = doc.createElement("class");
			classElement.setAttribute("name", classNode.getName());
			classElement.setAttribute("package", classNode.getPackageName());
			classesElement.appendChild(classElement);

			Element methodsElement = doc.createElement("methods");

			for (MethodNode methodNode : classNode.getMethodNodeList()){

				Element methodElement = doc.createElement("method");
				methodElement.setAttribute("name", methodNode.getCompleteName());
				methodElement.setAttribute("returnType", methodNode.getReturnType());

				Element sequenceElement = doc.createElement("callingSequence");

				//Generate method calling sequence
				buildSequenceNode(doc, sequenceElement, null, methodNode);

				methodElement.appendChild(sequenceElement);

				methodsElement.appendChild(methodElement);
			}

			classElement.appendChild(methodsElement);
		}
	}


	/**
	 * Creating an empty XML Document
	 * 
	 * @return
	 * @throws ParserConfigurationException
	 */
	private Document getNewDocument() throws ParserConfigurationException{

		return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
	}



	/**Get or create package and classes element
	 * @param root
	 * @param classNode
	 * @return classes element
	 * @throws TransformerException
	 */
	private Element getClassesElement(Element root, ClassNode classNode) throws TransformerException {
		Element classesElement = null;

		String xPathForPackage ="/root/package[@name= '"+classNode.getPackageName()+"']";

		Node packageNode = XPathAPI.selectSingleNode(doc, xPathForPackage);

		if(packageNode == null){//Package doesn't exit
			//Create package and classes element
			Element pkgElement = doc.createElement("package");
			pkgElement.setAttribute("name", classNode.getPackageName());
			root.appendChild(pkgElement);

			classesElement = doc.createElement("classes");
			pkgElement.appendChild(classesElement);

		}else{//Package element exits. There's only one child
			classesElement = (Element)packageNode.getFirstChild();
		}

		return classesElement;
	}






	private void buildSequenceNode(Document doc,Element sequenceElement, 
			MethodNode fromMethodNode, MethodNode toMethodNode) {

		if(fromMethodNode != null){
			Element nextMethod = doc.createElement("call");
			nextMethod.setAttribute("from", fromMethodNode.getFullPath());
			nextMethod.setAttribute("to", toMethodNode.getFullPath());

			sequenceElement.appendChild(nextMethod);
		}

		if(toMethodNode.getCallingMethodList() != null && toMethodNode.getCallingMethodList().size() > 0){

			for (MethodNode methodNode2 : toMethodNode.getCallingMethodList()) {
				buildSequenceNode(doc, sequenceElement, toMethodNode,  methodNode2);
			}
		}
	}






















	public void writeXmlFile() throws Exception { 
		writeXmlFile(doc, "graph.xml");
	}


	/**
	 * This method writes a DOM document to a file 
	 * 
	 * @param doc Document
	 * @param filename file path
	 * @throws Exception
	 */
	private void writeXmlFile(Document doc, String filename) throws Exception { 


		// Prepare the DOM document for writing Source 
		DOMSource source = new DOMSource(doc); 
		// Prepare the output file 
		File file = new File(filename); 
		Result result = new StreamResult(file.toURI().getPath()); 
		// Write the DOM document to the file 
		Transformer xformer = TransformerFactory.newInstance().newTransformer();
		xformer.transform(source, result);

		//Debug
		System.out.println("XML created: "+file.toURI().getPath());

	}


	/**Add to the DOM, from method Element, Variables and Edges
	 * @param method
	 * @throws ElementNotFoundException 
	 * @throws TransformerException 
	 */
	public void updateDOM(MethodNode method) throws ElementNotFoundException, TransformerException  {


		String xPathForMethod ="/root/package[@name= '"+method.getPackageName()+"']" +
			"/classes/class[@name= '"+method.getClassName()+"']" +
			"/methods/method[@name= '"+method.getCompleteName()+"']";


		Element methodElement = (Element)XPathAPI.selectSingleNode(doc, xPathForMethod);
		if(methodElement == null){
			throw new ElementNotFoundException("Node not found, XPath: "+xPathForMethod);
		}

		createVariables(method, methodElement);

		createEdges(method, methodElement);



	}

	/**Add all variables element to DOM on method Element
	 * @param method
	 * @param methodElement
	 */
	private void createVariables(MethodNode method, Element methodElement) {
		Element variablesElement = doc.createElement("variables");

		//Create Foo Node
		createAppendVariableElement(method.getFooNode(), variablesElement);

		//Create Context Node
		createAppendVariableElement(method.getContextNode(), variablesElement);


		//Create Reference Nodes
		for (GraphNode graphNodeRef : method.getGraphNodeRefList()) {
			createAppendVariableElement(graphNodeRef, variablesElement);
		}

		//Create Memory Nodes
		for (GraphNode graphNodeMem : method.getGraphNodeMemList()) {
			createAppendVariableElement(graphNodeMem, variablesElement);
		}

		methodElement.appendChild(variablesElement);
	}


	/**Create variable element and append to variables element
	 * @param graphNode Variable Info
	 * @param variablesElement to append variable element 
	 */
	private void createAppendVariableElement(GraphNode graphNode, Element variablesElement) {
		Element variableElement = doc.createElement("variable");
		variableElement.setAttribute("name", graphNode.getLabel());
		variableElement.setAttribute("type", graphNode.getType().toString());
		if(graphNode.getValueType()!= null){
			variableElement.setAttribute("valueType", graphNode.getValueType());
		}
		if(graphNode.getOrigin() != null){
			variableElement.setAttribute("origin", graphNode.getOrigin().toString());
			if(graphNode.getOrigin().equals(GraphNode.NoteOrigin.PARAM)){
				variableElement.setAttribute("position", String.valueOf(graphNode.getPosition()));
			}
		}

		variablesElement.appendChild(variableElement);

	}


	/**Create Read, Write and Pointee Edges to method element
	 * @param method contains edges info
	 * @param edgesElement
	 */
	private void createEdges(MethodNode method, Element methodElement) {

		Element edgesElement = doc.createElement("edges");
		edgesElement.setAttribute("context", method.getContext());

		//Create Read Edges
		for (Edge edge : method.getReadEdgeList()) {
			createEdge(edgesElement, edge);
		}

		//Create Write Edges
		for (Edge edge : method.getWriteEdgeList()) {
			createEdge(edgesElement, edge);
		}

		//Create Pointee Edges
		for (Edge edge : method.getPointeeEdgeList()) {
			createEdge(edgesElement, edge);
		}

		methodElement.appendChild(edgesElement);
	}



	/**Create Edge Element on edgesElement
	 * @param edgesElement
	 * @param edge
	 */
	private void createEdge(Element edgesElement, Edge edge) {
		Element edgeElement = doc.createElement("edge");
		edgeElement.setAttribute("from", edge.getFrom().getLabel());
		edgeElement.setAttribute("to", edge.getTo().getLabel());
		edgeElement.setAttribute("type", edge.getEdgeType());
		edgesElement.appendChild(edgeElement);
	}




	/**Generate Access Permission on DOM, 
	 * then extract results from DOM to Class structure. 
	 * @param methodNode
	 * @throws Exception
	 */
	public void generateAccessPermission(MethodNode methodNode) throws Exception  {



		pt.uma.gap.accessperm.GAP.generateAccessPermisisonsForMethod(doc, methodNode.getPackageName(), 
				methodNode.getClassName(), methodNode.getCompleteName());



		//Load access Permission for each Reference Node
		NamedNodeMap attributes;
		String accessPremission;
		for (GraphNode grahpNodeRef : methodNode.getGraphNodeRefList()) {

			String xPathForVar = "/root/package[@name= '"
					+ methodNode.getPackageName() + "']"
					+ "/classes/class[@name= '" + methodNode.getClassName()
					+ "']" + "/methods/method[@name= '"
					+ methodNode.getCompleteName() + "']"
					+ "/variables/variable[@name='" + grahpNodeRef.getLabel()
					+ "' and @type='REF']";

			Node variableNode = XPathAPI.selectSingleNode(doc, xPathForVar);

			if(variableNode == null){
				throw new ElementNotFoundException("Node not found, XPath: "+xPathForVar);
			}
			attributes = variableNode.getAttributes();
			try {
				accessPremission = attributes.getNamedItem("postAccessPermission").getNodeValue();
				grahpNodeRef.setPostAccessPerm(GraphNode.AccessPerm.valueOf(accessPremission));
			} catch (Exception e) {
				logger.log(Level.SEVERE,
						"Post Access Permission not loaded for variable: \""
								+ grahpNodeRef.getLabel() + "\" from method: \""+methodNode.getFullPath()+"\"");
			}
			try {
				accessPremission = attributes.getNamedItem("preAccessPermission").getNodeValue();
				grahpNodeRef.setPreAccessPerm(GraphNode.AccessPerm.valueOf(accessPremission));
			} catch (Exception e) {
				logger.log(Level.SEVERE,
						"Pre Access Permission not loaded for variable: \""
						+ grahpNodeRef.getLabel() + "\" from method: \""+methodNode.getFullPath()+"\"");
			}
		}

		methodNode.setAccessPermLoaded(true);

	}







}
